/*
 * Copyright 1999-2011 Alibaba Group.
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.dubbo.common.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.LinkedList;

/**
 * UnsafeChunkByteArrayOutputStream.
 * 
 * @author junbo.ruan
 */

public class UnsafeChunkByteArrayOutputStream extends OutputStream{
	static public final int CHUNK_SIZE = 4096;
 
	protected int chunkSize = CHUNK_SIZE;
	
	LinkedList<UnsafeChunk> buffer;
	LinkedList<UnsafeChunk> unused;
	int count = 0;
	
	public UnsafeChunkByteArrayOutputStream()
	{
		this(CHUNK_SIZE);
    }
	
	
	public UnsafeChunkByteArrayOutputStream(int size)
	{
		this(size, CHUNK_SIZE);
    }
	
	public UnsafeChunkByteArrayOutputStream(int size, int chunkSize)
	{
		if(chunkSize < CHUNK_SIZE) chunkSize =  CHUNK_SIZE;
		this.chunkSize = chunkSize;
		
		buffer = new LinkedList<UnsafeChunk>();
		unused = new LinkedList<UnsafeChunk>();
		
		int count = (size + this.chunkSize -1)/this.chunkSize;
		UnsafeChunk chunk = new UnsafeChunk(this.chunkSize);
		buffer.addLast(chunk);
		for(int i = 1;i<count;i++){
			chunk = new UnsafeChunk(this.chunkSize);
			unused.addLast(chunk);
		}
    }
	
	 

	public void ensureIncreaseCapacity(int len){
		int needChunks = 0;
		if(!buffer.isEmpty()){
			UnsafeChunk chunk = buffer.getLast();
			if(chunk.available() >= len) return;
			needChunks = (len - chunk.available() + this.chunkSize -1)/this.chunkSize;
		}else{
			needChunks = (len + this.chunkSize -1)/this.chunkSize;
		}
		
		int newChunks = needChunks - unused.size();
		if(newChunks > 0){
			for(int i = 0;i<newChunks;i++){
				UnsafeChunk chunk = new UnsafeChunk(this.chunkSize);
				unused.addLast(chunk);
			}
		}
	}
	
	public void ensureCapacity(int len){
		if( len <= 0)
			throw new java.lang.IllegalArgumentException();
		int total = 0;
		total += this.chunkSize * buffer.size();
		if(len <= total) return;
		ensureIncreaseCapacity(len - total);
	}
	
	public void write(int b)
	{
		if(!this.buffer.isEmpty()){
			UnsafeChunk chunk = this.buffer.getLast();
			if(chunk.available() > 0){
				chunk.write((byte)b);
				this.count ++;
				return;
			}
		}
		
		UnsafeChunk chunk = null;
		if(this.unused.isEmpty()){
			chunk = new UnsafeChunk(this.chunkSize);
		}else{
			chunk = this.unused.removeFirst();
		}
		chunk.write((byte)b);
		this.buffer.addLast(chunk);
		this.count ++;
	}

	public void write(byte b[], int off, int len)
	{
		if( ( off < 0 ) || ( off > b.length ) || ( len < 0 ) || ( ( off + len ) > b.length ) || ( ( off + len ) < 0 ) )
			throw new IndexOutOfBoundsException();
		if( len == 0 )
			return;
		ensureIncreaseCapacity(len);
		int offset = off;
		int length = len;
		if(!this.buffer.isEmpty()){
			UnsafeChunk chunk = this.buffer.getLast();
			int written = chunk.write(b, offset, length);
			offset += written;
			length -= written;
			this.count += written;
		}
		
		while(length > 0){
			UnsafeChunk chunk = this.unused.removeFirst();
			this.buffer.addLast(chunk);
			int written = chunk.write(b, offset, length);
			offset += written;
			length -= len;
			this.count += written;
		}
	}

	public int size()
	{
		return this.count;
	}

	public void reset()
	{
		while(buffer.size() > 1){
			UnsafeChunk chunk = buffer.removeFirst();
			chunk.reset();
			this.unused.addLast(chunk);
		}
		
		if(!buffer.isEmpty()){
			buffer.getFirst().reset();
		}
		
		this.count = 0;
	}

	public byte[] toByteArray()
	{
		int total = this.size();
		if(total == 0) return ByteArrayOutputStream.ZERO;
		byte [] data = new byte[total];
		int offset = 0;
		for(UnsafeChunk chunk : this.buffer){
			System.arraycopy(chunk.data(), 0, data, offset,  chunk.size());
			offset += chunk.size();
		}
		return data;
	}

	public ByteBuffer toByteBuffer()
	{
		return ByteBuffer.wrap(toByteArray());
	}

	public void writeTo(OutputStream out) throws IOException
	{
		if(!this.buffer.isEmpty()){
			for(UnsafeChunk chunk : this.buffer){
				out.write(chunk.data(), 0, chunk.size());
			}			
		}
	}

	public String toString()
	{
		byte[] data = toByteArray();
		return new String(data, 0, data.length);
	}
	
	public ByteArray detach(){
		if(this.buffer.size() ==  1){
			UnsafeChunk chunk = this.buffer.removeFirst();
			return new ByteArray(chunk.data(), 0, chunk.size());
		}
		byte []data = this.toByteArray();
		this.buffer.clear();
		return new ByteArray(data, 0, data.length);
	}

	public String toString(String charset) throws UnsupportedEncodingException
	{
		byte[] data = toByteArray();
		return new String(data, 0, data.length, charset);
	}
	
	
	public void writeInt8(int index, byte b){
		if(index >= this.count) throw new IndexOutOfBoundsException();
		//快速定义index所在的chunk，及所在偏移
		int ch = index / this.chunkSize;
		int off = index % this.chunkSize;
		UnsafeChunk chunk = this.buffer.get(ch);
		chunk.data()[off] = b;
	}
	
	public void writeInt32b(int b){
		this.ensureIncreaseCapacity(4);
		UnsafeChunk chunk = this.buffer.getLast(); 
		int available = chunk.available();
		UnsafeChunk []bs = new UnsafeChunk[2];
		if(available >= 4){
			bs[0] = chunk;
		}else if(available != 0){
			bs[0] = chunk;
			bs[1] = this.unused.removeFirst();
			this.buffer.addLast(bs[1]);
		}else{
			bs[0] = this.unused.removeFirst();
			this.buffer.addLast(bs[1]);
		}
		
		int bytes = 4;
		int c = 0;
		int shift = (bytes -1) * 8;
		for(int i = 0;i < bytes;i++){
			chunk = bs[c];
			byte [] data = chunk.data();
			int off = chunk.size();
			data[off] = shift == 0? (byte)b:(byte)(b >>> shift);
			chunk.skip();
			if(chunk.available() == 0) c ++;
			shift -= 8;
		}
		this.count += bytes;
	}
	
	 
	public void writeInt16b(short b){
		this.ensureIncreaseCapacity(2);
		UnsafeChunk chunk = this.buffer.getLast(); 
		int available = chunk.available();
		UnsafeChunk []bs = new UnsafeChunk[2];
		if(available >= 2){
			bs[0] = chunk;
		}else if(available != 0){
			bs[0] = chunk;
			bs[1] = this.unused.removeFirst();
			this.buffer.addLast(bs[1]);
		}else{
			bs[0] = this.unused.removeFirst();
			this.buffer.addLast(bs[1]);
		}
		
		int bytes = 2;
		int c = 0;
		int shift = (bytes -1) * 8;
		for(int i = 0;i < bytes;i++){
			chunk = bs[c];
			byte [] data = chunk.data();
			int off = chunk.size();
			data[off] = shift == 0? (byte)b:(byte)(b >>> shift);
			chunk.skip();
			if(chunk.available() == 0) c ++;
			shift -= 8;
		}
		this.count += bytes;
	}
	
	
	 
	public void writeInt32b(int index, int b){
		if(index < 0 || (index + 4) > this.count) throw new IndexOutOfBoundsException();
		int ch = index / this.chunkSize;
		int offset = index % this.chunkSize;
		
		UnsafeChunk chunk = this.buffer.get(ch); 
		int available = chunk.size() - offset;
		UnsafeChunk []bs = new UnsafeChunk[2];
		if(available >= 4){
			bs[0] = chunk;
		}else {
			bs[0] = chunk;
			bs[1] = this.buffer.get(ch+1);
		} 
		
		int bytes = 4;
		int c = 0;
		int shift = (bytes -1) * 8;
		byte []data = bs[c].data();
		for(int i = 0;i < bytes;i++){
			data[offset] = shift == 0? (byte)b:(byte)(b >>> shift);
			offset ++;
			offset %= this.chunkSize;
			if(offset == 0){
				c ++;
				data = bs[c].data();
			}
			shift -= 8;
		}
	}
	
	
 
	
	public void writeInt16b(int index, short b){
		if(index < 0 || (index + 2) > this.count) throw new IndexOutOfBoundsException();
		int ch = index / this.chunkSize;
		int offset = index % this.chunkSize;
		
		UnsafeChunk chunk = this.buffer.get(ch); 
		int available = chunk.size() - offset;
		UnsafeChunk []bs = new UnsafeChunk[2];
		if(available >= 2){
			bs[0] = chunk;
		}else {
			bs[0] = chunk;
			bs[1] = this.buffer.get(ch+1);
		} 
		
		int bytes = 2;
		int c = 0;
		int shift = (bytes -1) * 8;
		byte []data = bs[c].data();
		for(int i = 0;i < bytes;i++){
			data[offset] = shift == 0? (byte)b:(byte)(b >>> shift);
			offset ++;
			offset %= this.chunkSize;
			if(offset == 0){
				c ++;
				data = bs[c].data();
			}
			shift -= 8;
		}
	}
	
	 
	
	 
	public void writeInt64b(int index, long b){
		if(index < 0 || (index + 8) > this.count) throw new IndexOutOfBoundsException();
		int ch = index / this.chunkSize;
		int offset = index % this.chunkSize;
		
		UnsafeChunk chunk = this.buffer.get(ch); 
		int available = chunk.size() - offset;
		UnsafeChunk []bs = new UnsafeChunk[2];
		if(available >= 8){
			bs[0] = chunk;
		}else {
			bs[0] = chunk;
			bs[1] = this.buffer.get(ch+1);
		} 
		
		int bytes = 8;
		int c = 0;
		int shift = (bytes -1) * 8;
		byte []data = bs[c].data();
		for(int i = 0;i < bytes;i++){
			data[offset] = shift == 0? (byte)b:(byte)(b >>> shift);
			offset ++;
			offset %= this.chunkSize;
			if(offset == 0){
				c ++;
				data = bs[c].data();
			}
			shift -= 8;
		}
	}
	
 
	
	public void writeInt8(byte b){
		write(b);
	}
	
	public void writeInt8b(byte b){
		write(b);
	}
	
	public void writeInt8(int b){
		write(b);
	}
	
	public void writeInt8b(int b){
		write(b);
	}
	
	 
	
	public void writeInt64b( long b){
		this.ensureIncreaseCapacity(8);
		UnsafeChunk chunk = this.buffer.getLast(); 
		int available = chunk.available();
		UnsafeChunk []bs = new UnsafeChunk[2];
		if(available >= 8){
			bs[0] = chunk;
		}else if(available != 0){
			bs[0] = chunk;
			bs[1] = this.unused.removeFirst();
			this.buffer.addLast(bs[1]);
		}else{
			bs[0] = this.unused.removeFirst();
			this.buffer.addLast(bs[1]);
		}
		
		int bytes = 8;
		int c = 0;
		int shift = (bytes -1) * 8;
		for(int i = 0;i < bytes;i++){
			chunk = bs[c];
			byte [] data = chunk.data();
			int off = bs[c].size();
			data[off] = shift == 0? (byte)b:(byte)(b >>> shift);
			chunk.skip();
			if(chunk.available() == 0) c ++;
			shift -= 8;
			this.count ++;
		}
	}

	public void close() throws IOException
	{
		this.count = 0;
		this.buffer.clear();
		this.unused.clear();
	}
	
	public UnsafeChunkByteArrayInputStream toUnsafeChunkByteArrayInputStream(){
		UnsafeChunkByteArrayInputStream os = new UnsafeChunkByteArrayInputStream(this.buffer);
		this.buffer = new LinkedList<UnsafeChunk>();
		this.count = 0;
		return os;
	}

	static public void main(String []args) throws IOException{
		UnsafeChunkByteArrayOutputStream s = new UnsafeChunkByteArrayOutputStream();
		s.write(0x1);
		s.writeInt32b(0x12345678);
		s.writeInt64b(0x123456789abcdef0L);
		StringBuilder sb = new StringBuilder();
		for(int i = 0;i< 500;i++){
			sb.append("i am from china");
		}
		byte []d = sb.toString().getBytes();
		s.write(d, 0, d.length);
		s.writeInt64b(4094, 0x123456789abcdef0L);
		UnsafeChunkByteArrayInputStream xs = s.toUnsafeChunkByteArrayInputStream();
		xs.read(d, 0, d.length);
		s.close();
	}
}